{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Creating animations with Flat and ffmpeg\n",
    "\n",
    "By [Allison Parrish](http://www.decontextualize.com)\n",
    "\n",
    "This notebook demonstrates how to produce animations with [Flat](https://xxyxyz.org/flat), [ipywidgets](https://ipywidgets.readthedocs.io/) and [ffmpeg](https://ffmpeg.org/). The notebook builds on examples from [Material of Language](https://github.com/aparrish/material-of-language/), and in particular assumes that you're familiar with the material in this [interactive widgets tutorial](interactive-widgets.ipynb). In the notebook, I show two techniques for generating animations:\n",
    "\n",
    "* Use [the ipywidgets Play widget](https://ipywidgets.readthedocs.io/en/latest/examples/Widget%20List.html)\n",
    "* Export an image sequence, then convert the image sequence to a video file with `ffmpeg`\n",
    "\n",
    "> Note: There are a number of ways to convert image sequences to video files: here are [instructions for QuickTime Player in macOS Catalina 10.15](https://support.apple.com/guide/quicktime-player/create-a-movie-with-an-image-sequence-qtp315cce984/mac); or see documentation on [similar functionality in Adobe Premiere Pro](https://helpx.adobe.com/premiere-pro/using/importing-still-images.html) and [After Effects](https://helpx.adobe.com/after-effects/using/preparing-importing-still-images.html#import_a_single_still_image_or_a_still_image_sequence). If you have access to those tools and prefer to use them, then you can ignore the content about ffmpeg below.\n",
    "\n",
    "Make sure you have ffmpeg installed before you begin. If you're using macOS, I again recommend using [Homebrew](https://brew.sh/) to install it: `brew install ffmpeg`. You can also install it with Anaconda:\n",
    "\n",
    "    conda install -c conda-forge ffmpeg\n",
    "\n",
    "The notebook assumes that the `ffmpeg` command is available in your shell's path. To check, try running the following:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "ffmpeg version 4.2 Copyright (c) 2000-2019 the FFmpeg developers\r\n",
      "  built with clang version 4.0.1 (tags/RELEASE_401/final)\r\n",
      "  configuration: --prefix=/Users/allison/opt/miniconda3/envs/material-of-language-2020 --cc=x86_64-apple-darwin13.4.0-clang --disable-doc --disable-openssl --enable-avresample --enable-gnutls --enable-gpl --enable-hardcoded-tables --enable-libfreetype --enable-libopenh264 --enable-libx264 --enable-pic --enable-pthreads --enable-shared --enable-static --enable-version3 --enable-zlib --enable-libmp3lame\r\n",
      "  libavutil      56. 31.100 / 56. 31.100\r\n",
      "  libavcodec     58. 54.100 / 58. 54.100\r\n",
      "  libavformat    58. 29.100 / 58. 29.100\r\n",
      "  libavdevice    58.  8.100 / 58.  8.100\r\n",
      "  libavfilter     7. 57.100 /  7. 57.100\r\n",
      "  libavresample   4.  0.  0 /  4.  0.  0\r\n",
      "  libswscale      5.  5.100 /  5.  5.100\r\n",
      "  libswresample   3.  5.100 /  3.  5.100\r\n",
      "  libpostproc    55.  5.100 / 55.  5.100\r\n",
      "Hyper fast Audio and Video encoder\r\n",
      "usage: ffmpeg [options] [[infile options] -i infile]... {[outfile options] outfile}...\r\n",
      "\r\n",
      "\u001b[0;33mUse -h to get full help or, even better, run 'man ffmpeg'\r\n",
      "\u001b[0m"
     ]
    }
   ],
   "source": [
    "!ffmpeg"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "If you get a `command not found` error message (or similar), then you either don't have ffmpeg installed, or ffmpeg isn't in your shell's path—you'll have to fix that problem before you proceed! (If you're in my class, don't hesitate to send me an e-mail and I can help you sort it out.\n",
    "\n",
    "Some code preliminaries:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "from IPython.display import display, Image, HTML"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "import ipywidgets as widgets"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "from flat import document, shape, rgb"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The cell below takes a Flat `page` object and displays it inline in Jupyter notebook:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [],
   "source": [
    "from IPython.display import SVG, display\n",
    "def show(page):\n",
    "    display(SVG(page.svg()))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Animating with ipywidgets `Play`\n",
    "\n",
    "For our purposes, we'll define an \"animation\" as a series of images (or \"frames\") that are shown in succession quickly enough that they can produce the illusion of motion (taking advantage of [beta movement](https://en.wikipedia.org/wiki/Beta_movement)). Our strategy for producing an animation in Python is to write a function that takes an integer parameter that indicates the current frame index, and return an image for the corresponding frame. This is close to the way that (e.g.) the `draw()` function in [p5.js](https://p5js.org/) works, except there is no canvas that persists between frames, and you have to call the function yourself for each of the frames you want to generate.\n",
    "\n",
    "The primary strategy for generating an animation in this notebook is writing a function like this and then calling it in a loop. But a super easy way to experiment with animation is [ipywidget's `Play` control](https://ipywidgets.readthedocs.io/en/latest/examples/Widget%20List.html#Play-(Animation)-widget). This widget produces a sequence of increasing integers after you press a \"Play\" button, which you can connect to a function with `widgets.interact()`. Here's a quick example that just prints those integers:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [],
   "source": [
    "def print_val(step=0):\n",
    "    print(step)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "cf9f36dc059444d3b714bb51a40d9b72",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "interactive(children=(Play(value=0, description='step', max=30), Output()), _dom_classes=('widget-interact',))"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/plain": [
       "<function __main__.print_val(step=0)>"
      ]
     },
     "execution_count": 12,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "play_widget = widgets.Play(min=0, max=30, interval=100)\n",
    "widgets.interact(print_val, step=play_widget)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "When you press the play button, you should see an increasing series of integers. The `interval` parameter sets the number of milliseconds that the `Play` widget waits between calls to the function.\n",
    "\n",
    "To animate a Flat drawing, take your existing (non-animated) code and put it in a function that accepts a parameter and returns a Flat `page` object. The parameter will receive the value from the `Play` widget, which you can think of as the number of the frame that the function should draw in your animation. (In the following examples, I've called this parameter `step`, but you can call it whatever you want.)\n",
    "\n",
    "The code below implements a simple example of this:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [],
   "source": [
    "def render(step=0):\n",
    "    page = document(80, 80, 'mm').addpage()\n",
    "    page.place(shape().nostroke().fill(rgb(255, 255, 255)).rectangle(0, 0, 80, 80))\n",
    "    page.place(shape().nostroke().fill(rgb(40, 40, 160)).ellipse(40, 40, step, step*1.5))\n",
    "    return page"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "As specified above, this function takes a parameter `step` and returns a Flat `page`. The code in the function creates the page, draws a white rectangle on the page (an opaque background), and then draws a blue ellipse over that rectangle. The size of the ellipse is determined by the `step` parameter.\n",
    "\n",
    "You can call this function with an integer to see what the animation looks like on the corresponding frame."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/svg+xml": [
       "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" version=\"1.1\" width=\"226.7717pt\" height=\"226.7717pt\">\n",
       "<title>Untitled</title>\n",
       "<rect x=\"0\" y=\"0\" width=\"226.7717\" height=\"226.7717\" fill=\"rgb(255,255,255)\"/>\n",
       "<ellipse cx=\"113.3858\" cy=\"113.3858\" rx=\"14.1732\" ry=\"21.2598\" fill=\"rgb(40,40,160)\"/>\n",
       "</svg>"
      ],
      "text/plain": [
       "<IPython.core.display.SVG object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "show(render(5))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/svg+xml": [
       "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" version=\"1.1\" width=\"226.7717pt\" height=\"226.7717pt\">\n",
       "<title>Untitled</title>\n",
       "<rect x=\"0\" y=\"0\" width=\"226.7717\" height=\"226.7717\" fill=\"rgb(255,255,255)\"/>\n",
       "<ellipse cx=\"113.3858\" cy=\"113.3858\" rx=\"99.2126\" ry=\"148.8189\" fill=\"rgb(40,40,160)\"/>\n",
       "</svg>"
      ],
      "text/plain": [
       "<IPython.core.display.SVG object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "show(render(35))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now we can animate this with the `Play` widget, although there is a bit of glue that we need first to put everything together. The `render()` function doesn't actually *show* the page, it just *returns* the page. So in order to use this function with `widgets.interact()`, you need a second little function that just shows the page that `render()` returns:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [],
   "source": [
    "def render_and_show(step=0):\n",
    "    show(render(step))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now you can call `widgets.interact()` with this function and a `Play` widget:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "297e68675f3a4fc1a52a9714a2f60161",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "interactive(children=(Play(value=0, description='step', interval=33, max=30), Output()), _dom_classes=('widget…"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/plain": [
       "<function __main__.render_and_show(step=0)>"
      ]
     },
     "execution_count": 19,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "widgets.interact(render_and_show, step=widgets.Play(min=0, max=30, interval=1000/30))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Nice! Note the `interval` parameter: `1000/30` gives us one thousand milliseconds (=one second) divided by thirty, i.e., the number of milliseconds between frames if you want to show thirty frames per second."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Exporting image sequences\n",
    "\n",
    "The technique above (using the `Play` widget) might be enough for you! There is no way to export a video file directly using the `Play` widget, but you could use [licecap](https://www.cockos.com/licecap/) to capture a GIF right from the screen, or do a screen recording with (e.g.) QuickTime Player. The primary disadvantage is that you can't control the actual frame rate: the `Play` widget attempts to update every `interval` milliseconds, but may not actually succeed in drawing that frequently (if, for example, your computer isn't fast enough to render your scene that quickly). You're also limited to generating images whose resolution is less than the size of your browser window.\n",
    "\n",
    "If you want a bit more control over timing and size of your animations, the most flexible workflow involves generating a sequence of image files, then converting those image files to a video file. This is a standard workflow, and is supported by a number of video editing tools. In order to keep things programmatic, open source and cross-platform, we'll be using the excellent [ffmpeg](https://ffmpeg.org/), a command line tool for editing video files.\n",
    "\n",
    "### Generating images with Flat\n",
    "\n",
    "In order to produce a *sequence* of images, we first need to be able to generate a *single* image. Flat provides a built-in means of doing this: the `.image()` method of a `page` object. For example, using the `render()` function above:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 31,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "b'\\x89PNG\\r\\n\\x1a\\n\\x00\\x00\\x00\\rIHDR\\x00\\x00\\x00\\xe3\\x00\\x00\\x00\\xe3\\x08\\x02\\x00\\x00\\x00\\xf8\\xec4\\x1b\\x00\\x00\\x08;IDATx\\x9c\\xed\\xdd\\xffOUu\\x1c\\xc7\\xf1\\xfe\\x1c\\x01q:\\xa5\\xcd\\xb1U*}qm\\xfd\\x90\\xcb\\xd5V\\xb3\\xdc\\xe4\\x17W\\xce\\xda,g_f?\\x94\\x99mn5\\x9a+\\x9b\\x17\\xb9\\xa8\\t\\x06ffA\\x90%'"
      ]
     },
     "execution_count": 31,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "page = render(10)\n",
    "png_bytes = page.image(ppi=72, kind='rgb').png()\n",
    "png_bytes[:100]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This chain of method calls evaluates to the binary data of a PNG image, which we could then write to disk. The problem with this technique is that Flat's PNG renderer is *very slow*, potentially taking several hundred milliseconds to render even very simple drawings. A hundred milliseconds doesn't sound like much, but when you're rendering thousands of frames, it can easily add up.\n",
    "\n",
    "For this reason, I recommend using [CairoSVG](https://cairosvg.org/) to render Flat-produced SVG code as PNG images. In my initial round of benchmarking, CairoSVG renders Flat page objects to PNG 30x-50x faster than Flat's built-in code."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Install CairoSVG with Anaconda, which will also install tricky system dependencies:\n",
    "\n",
    "    conda install -c conda-forge cairosvg\n",
    "    \n",
    "If you're not using Anaconda, you can install using `pip`:\n",
    "\n",
    "    pip install cairosvg\n",
    "    \n",
    "Though I *think* installing from `pip` will lead to trouble if you don't already have [Cairo](https://www.cairographics.org/) installed. (If you're on macOS, I recommend using [Homebrew](https://brew.sh/) to install Cairo: `brew install cairo`.)\n",
    "\n",
    "If none of that works, don't fret—you can use still Flat's built-in PNG renderer. (Just remember that this is a bit slower than using CairoSVG.) The function below takes a Flat page as a parameter, then converts it to a PNG byte string, using CairoSVG if it's available and the Flat's PNG renderer otherwise:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 35,
   "metadata": {},
   "outputs": [],
   "source": [
    "def page2png(page, dpi=72):\n",
    "    try:\n",
    "        import cairosvg\n",
    "        png_bytes = cairosvg.svg2png(page.svg(), dpi=dpi)\n",
    "    except ModuleNotFoundError:\n",
    "        png_bytes = page.image(ppi=dpi, kind='rgb').png()\n",
    "    return png_bytes"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Testing it out:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 36,
   "metadata": {},
   "outputs": [],
   "source": [
    "png_data = page2png(render(10))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 37,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "b'\\x89PNG\\r\\n\\x1a\\n\\x00\\x00\\x00\\rIHDR\\x00\\x00\\x00\\xe2\\x00\\x00\\x00\\xe2\\x08\\x02\\x00\\x00\\x00\\xdcr\\x8c\\x80\\x00\\x00\\x00\\x06bKGD\\x00\\xff\\x00\\xff\\x00\\xff\\xa0\\xbd\\xa7\\x93\\x00\\x00\\x08\\xf9IDATx\\x9c\\xed\\xddklTe\\x1e\\xc7\\xf1gfJ;\\xb0\\xed\\xf6fK\\xb7\\\\\\x04j\\x97\\xb2\\x91\\xda\\xe5\"\\xd9\\x94\\xab1@\\xb2\\x98\\x80\\x1aHT'"
      ]
     },
     "execution_count": 37,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "png_data[:100]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The function returns raw PNG data. You can see what it looks like using IPython's `Image()` function, which will automatically recognize the data format and show the image in the notebook:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 38,
   "metadata": {
    "scrolled": true
   },
   "outputs": [
    {
     "data": {
      "image/png": "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39q1szZseCTOi0rwOY5jewaXqqtr2bjx+7g9/3bmzJSKinUe/wRpJOymEeXnp7333t/ic5KanDzp009X0mgkZHo/69fnbdv211ivkpDg/+STlXYvL7gcmUbxyiuFzz9fELvjBwK+XbuKi4tzY7fEBMC56Yh89dUvpaWV437YxMTARx8tW7OGm0qjINORKi+v+fDDM+HwuP24UlOTyspWLVrEJ/KiI9NRuHjx1vbtx65duzv2QxUWPlRaumL69OSxH8oLyHR02tq6du786cCByw/8YwsGE7Zsmb9ly/xAwMVPB3AZMn0Q1dXNu3dXnj59Y1T/yu/3rV798JtvLszNZRMdHTJ9cPX1Lfv3X66oqGtpifK9t1OnTnnqqTmbNs3lXf7BkOlYdXWFq6ubz58PVVXdCoXaW1o629t7UlImpaUlTZuWXFiYtWBBdl5eqt/PW/yDI1MI4PI+BJApBJApBJApBJApBJApBJApBJApBJApBJApBJApBJApBJApBJApBJApBJApBJApBJApBJApBJApBJApBJApBJApBJApBJApBJApBJApBJApBJApBJApBJApBJApBJApBJApBJApBJApBJApBJApBJApBJApBJApBJApBJApBJApBJApBJApBJApBJApBJApBJApBJApBJApBJApBJApBJApBJApBJApBJApBJApBJApBJApBJApBJApBJApBJApBJApBJApBJApBJApBPwXhvk2Amgi1bUAAAAASUVORK5CYII=\n",
      "text/plain": [
       "<IPython.core.display.Image object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "display(Image(png_data))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Putting together filenames\n",
    "\n",
    "Okay, so now we can generate image data from a Flat page. To generate an animation, we need to generate an image for each frame of our animation, and then write that image out to disk as a file. We're potentially going to generate a *lot* of images, so it'll probably be best to put them in a directory on their own. The code in the following cell makes a subdirectory called `render` in the same directory as this notebook:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 39,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "mkdir: render: File exists\r\n"
     ]
    }
   ],
   "source": [
    "!mkdir render"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "If you just wanted to write a single image, you could do the following:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 42,
   "metadata": {},
   "outputs": [],
   "source": [
    "with open(\"render/test.png\", \"wb\") as fh:\n",
    "    fh.write(page2png(render(10)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Use your file browser to check the `render` subdirectory. You should see a file called `test.png` in there. ([More information on reading and writing files in Python](https://realpython.com/read-write-files-python/).)\n",
    "\n",
    "But we need to generate *multiple* frames, and each frame needs its own filename. Furthermore, those filenames should indicate the order in which the image falls in the sequence. The code in the following cell shows how to use a Python f-string to generate a filename for ten frames in a `for` loop:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 43,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "render/image00000.png\n",
      "render/image00001.png\n",
      "render/image00002.png\n",
      "render/image00003.png\n",
      "render/image00004.png\n",
      "render/image00005.png\n",
      "render/image00006.png\n",
      "render/image00007.png\n",
      "render/image00008.png\n",
      "render/image00009.png\n"
     ]
    }
   ],
   "source": [
    "for i in range(10):\n",
    "    fname = f\"render/image{i:05}.png\"\n",
    "    print(fname)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Note that this code doesn't actually generate the images—I just wanted to show how to create the filenames. The leading zeroes are important—ffmpeg expects them, and in any case the files wouldn't strictly be in alphabetical order without them."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Writing each frame\n",
    "\n",
    "The code below puts it all together. Looping from zero to thirty, it creates a filename, generates PNG data for that frame, then writes the PNG data to the file:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 48,
   "metadata": {},
   "outputs": [],
   "source": [
    "for i in range(30):\n",
    "    fname = f\"render/image{i:05}.png\"\n",
    "    png_data = page2png(render(i))\n",
    "    with open(fname, \"wb\") as fh:\n",
    "        fh.write(png_data)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 49,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "image00000.png image00006.png image00012.png image00018.png image00024.png\r\n",
      "image00001.png image00007.png image00013.png image00019.png image00025.png\r\n",
      "image00002.png image00008.png image00014.png image00020.png image00026.png\r\n",
      "image00003.png image00009.png image00015.png image00021.png image00027.png\r\n",
      "image00004.png image00010.png image00016.png image00022.png image00028.png\r\n",
      "image00005.png image00011.png image00017.png image00023.png image00029.png\r\n"
     ]
    }
   ],
   "source": [
    "!ls render"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The code in the cell below should open your operating system's file browser (e.g., macOS Finder) and show the `render` directory. If you enable the file browser's preview function, you can move up and down the file listing and see the animation."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 50,
   "metadata": {},
   "outputs": [],
   "source": [
    "!open render"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now we can use [ffmpeg](https://ffmpeg.org/) to convert the image sequence to an animation. The following command uses ffmpeg's `image2` input format, reads from the image sequence, then writes out an `.mp4` file with the H.264 codec and `yuv420p` pixel format (all good defaults for creating cross-platform video files):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 51,
   "metadata": {},
   "outputs": [],
   "source": [
    "!ffmpeg -loglevel warning -y \\\n",
    "    -framerate 30 -f image2 -i render/image%05d.png \\\n",
    "    -vcodec libx264 -crf 10 -pix_fmt yuv420p render/output.mp4"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "You should see a file called `output.mp4` in the `render` directory that has one second of video.\n",
    "\n",
    "The cell above runs the `ffmpeg` command installed on your system. It's all one command, even though it's on multiple lines—as in Python, the backslash character (`\\`) indicates a line continuation. (I separated it into multiple lines to make it a bit easier to read.) Some explanations of the command line options:\n",
    "\n",
    "* `-loglevel warning`: Restrict output to warnings; without this ffmpeg will print a page or two of informational output. (Which looks cool but isn't terribly interesting in most cases.)\n",
    "* `-y`: Always overwrite existing files without asking for confirmation. (Without this option, ffmpeg will hang if it has to ask you a question when you run it from Jupyter Notebook.)\n",
    "* `-f image2`: Use the `image2` format for input. (This is how you get `ffmpeg` to read a sequence of images.)\n",
    "* `-i render/image%05d.png`: The input parameter for `image2`, which tells `ffmpeg` to look for files whose names match a particular pattern. Check the ffmpeg documentation for [more information about valid filename patterns](https://www.ffmpeg.org/ffmpeg-formats.html#image2-1).\n",
    "* `-vcodec libx264`: This tells `ffmpeg` to use [H.264](https://en.wikipedia.org/wiki/Advanced_Video_Coding) as the output video codec (which is probably always what you want).\n",
    "* `-crf 10`: Controls the quality of the output. Lower is better. (A value of 0 is completely lossless but produces very large files; the maximum value is 51.) [More information on the `crf` parameter](https://trac.ffmpeg.org/wiki/Encode/H.264)\n",
    "* `-pix_fmt yuv420p`: Sets the pixel format for the output video; `yuv420p` is almost certainly what you want. (If you leave out this parameter or use a different value, the video might not work in certain players.)\n",
    "\n",
    "The final parameter is the name of the file that you want to write, the format of which `ffmpeg` guesses by the file extension you specify."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "To display the video in the notebook:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 63,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "\n",
       "    <video alt=\"rendered output\" controls>\n",
       "        <source src=\"render/output.mp4\" type=\"video/mp4\">\n",
       "    </video>\n"
      ],
      "text/plain": [
       "<IPython.core.display.HTML object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "from IPython.display import HTML\n",
    "display(HTML(\"\"\"\n",
    "    <video alt=\"rendered output\" controls>\n",
    "        <source src=\"render/output.mp4\" type=\"video/mp4\">\n",
    "    </video>\n",
    "\"\"\"))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "You can also make GIFs with `ffmpeg`. (This example is adapted from the code on this [wonderful Giphy Engineering blog post](https://engineering.giphy.com/how-to-make-gifs-with-ffmpeg/).)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 64,
   "metadata": {},
   "outputs": [],
   "source": [
    "!ffmpeg -loglevel warning -y \\\n",
    "    -framerate 30 -f image2 -i render/image%05d.png \\\n",
    "    -filter_complex \"[0:v] split [a][b];[a] palettegen [p];[b][p] paletteuse\" render/output.gif"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "To display a GIF in the notebook:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 62,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "R0lGODlh4gDiAPcAAAD/ACgooCkpoCoqoSsroSwsoi0toi4uoy8vozAwozAwpDExpDIypDMzpTQ0pTU1pjY2pjc3pzg4pzk5pzo6qDs7qDs7qTw8qT09qT4+qj8/qkBAqkBAq0FBq0JCrENDrERErEVFrUZGrUdHrkhIrklJr0pKr0tLr0tLsExMsE1NsE5OsU9PsVBQslFRslJSs1NTs1RUs1VVtFZWtFdXtVhYtVlZtlpatltbtltbt1xct11dt15euF9fuGBguWFhuWJiumNjumRkumVlu2Zmu2ZmvGdnvGhovGlpvWpqvWtrvWtrvmxsvm1tv25uv29vv3BwwHFxwHJywXNzwXR0wnV1wnZ2wnZ2w3d3w3h4w3l5xHp6xHt7xXx8xX19xn5+xn9/xoCAx4GBx4KCyIODyISEyYWFyYaGyYaGyoeHyoiIyomJy4qKy4uLzIyMzI2NzY6OzY+PzZCQzpGRzpGRz5KSz5OTz5SU0JWV0JaW0JaW0ZeX0ZiY0pmZ0pqa0pub05yc052d1J6e1J+f1aCg1aGh1aGh1qKi1qOj1qSk16Wl16am2Ken2Kio2amp2aqq2aur2qys2q2t266u26+v3LCw3LGx3LGx3bKy3bOz3bS03rW13ra237e337i44Lm54Lq64Lu74by84by84r294r6+4r+/48DA48HB48HB5MLC5MPD5cTE5cXF5cbG5sfH5sjI58nJ58rK6MvL6MzM6MzM6c3N6c7O6c/P6tDQ6tHR69LS69PT7NTU7NXV7NbW7dfX7djY7tnZ7tra79vb79zc79zc8N3d8N7e8N/f8eDg8eHh8uLi8uPj8+Tk8+Xl8+bm9Ofn9Ofn9ejo9enp9erq9uvr9uzs9uzs9+3t9+7u+O/v+PDw+PHx+fLy+fPz+vT0+vX1+/b2+/f3+/f3/Pj4/Pn5/Pr6/fv7/fz8/v39/v7+/////////////////////////////////////////////////////////////////////////////////yH/C05FVFNDQVBFMi4wAwEAAAAh+QQFAwAAACwAAAAA4gDiAIcA/wAoKKApKaAqKqErK6EsLKItLaIuLqMvL6MwMKMwMKQxMaQyMqQzM6U0NKU1NaY2NqY3N6c4OKc5Oac6Oqg7O6g7O6k8PKk9Pak+Pqo/P6pAQKpAQKtBQatCQqxDQ6xERKxFRa1GRq1HR65ISK5JSa9KSq9LS69LS7BMTLBNTbBOTrFPT7FQULJRUbJSUrNTU7NUVLNVVbRWVrRXV7VYWLVZWbZaWrZbW7ZbW7dcXLddXbdeXrhfX7hgYLlhYbliYrpjY7pkZLplZbtmZrtmZrxnZ7xoaLxpab1qar1ra71ra75sbL5tbb9ubr9vb79wcMBxccBycsFzc8F0dMJ1dcJ2dsJ2dsN3d8N4eMN5ecR6esR7e8V8fMV9fcZ+fsZ/f8aAgMeBgceCgsiDg8iEhMmFhcmGhsmGhsqHh8qIiMqJicuKisuLi8yMjMyNjc2Ojs2Pj82QkM6Rkc6Rkc+Sks+Tk8+UlNCVldCWltCWltGXl9GYmNKZmdKamtKbm9OcnNOdndSentSfn9WgoNWhodWhodaiotajo9akpNelpdemptinp9ioqNmpqdmqqtmrq9qsrNqtrduurtuvr9ywsNyxsdyxsd2yst2zs920tN61td62tt+3t9+4uOC5ueC6uuC7u+G8vOG8vOK9veK+vuK/v+PAwOPBwePBweTCwuTDw+XExOXFxeXGxubHx+bIyOfJyefKyujLy+jMzOjMzOnNzenOzunPz+rQ0OrR0evS0uvT0+zU1OzV1ezW1u3X1+3Y2O7Z2e7a2u/b2+/c3O/c3PDd3fDe3vDf3/Hg4PHh4fLi4vLj4/Pk5PPl5fPm5vTn5/Tn5/Xo6PXp6fXq6vbr6/bs7Pbs7Pft7ffu7vjv7/jw8Pjx8fny8vnz8/r09Pr19fv29vv39/v39/z4+Pz5+fz6+v37+/38/P79/f7+/v////////////////////////////////////////////////////////////////////////////////8I/wDZCRxIsKDBgwgTKlzIsKHDhxAjSpxIsaLFixgzatzIsaPHjyBDihxJsqTJkyhTqlzJsqXLlzBjypxJs6bNmzhz6tzJs6fPn0CDCh1KtKjRo0iTKl3KtKnTp1CjSp1KtarVq1izat3KtavXr2DDih1LtqzZs2jTql3Ltq3bt3Djyp1Lt67du3jz6t3Lt6/fv4ADCx5MuLDhw4gTK17MuLHjx5AjS55MubLly5gza97MubPnz6BDix5NurTp06hTq17NurXr17Bjy55Nu7bt27hz697N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\n",
      "text/plain": [
       "<IPython.core.display.Image object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "display(Image(open(\"render/output.gif\", \"rb\").read()))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Complete examples\n",
    "\n",
    "I've written some more complete examples below so you can see how this strategy works in practice. First, a few bits of code we'll use in both of the examples:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 142,
   "metadata": {},
   "outputs": [],
   "source": [
    "from bezmerizing import Polyline, Path\n",
    "from copy import copy\n",
    "import numpy as np"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Animating letters\n",
    "\n",
    "This example adapts the [\"one character at a time\" example from the Manipulating Fonts notebook](manipulating-font-data.ipynb#One-character-at-a-time). Instead of drawing the characters at random positions once, it draws characters at random positions and—as the number of frames progresses—moves those letters back to their \"original\" positions.\n",
    "\n",
    "Loading the font and copying over some helpful functions:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 143,
   "metadata": {},
   "outputs": [],
   "source": [
    "from flat import font"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 144,
   "metadata": {},
   "outputs": [],
   "source": [
    "f = font.open(\"NotoSans-Regular.ttf\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 145,
   "metadata": {},
   "outputs": [],
   "source": [
    "def glyphcommands(f, ch):\n",
    "    return Path([copy(cmd) for cmd in f.glyph(f.charmap[ord(ch)])])\n",
    "def advancefor(f, ch):\n",
    "    return f.advances[f.charmap[ord(ch)]]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Here's our source string (feel free to change this):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 86,
   "metadata": {},
   "outputs": [],
   "source": [
    "s = \"quartzy foxes\""
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "And the `render()` function for this example. Some notes:\n",
    "\n",
    "* You *need* to draw a background for the video to work! Flat pages have a transparent background by default, and the encoded video will show transparent backgrounds as all black. If you want the letters in this example to show up, you need to draw a white background on every frame.\n",
    "* If you're using a random number generator, either seed the generator with the same seed on every frame (as discussed in the [interactive widgets notebook](interactive-widgets.ipynb)) or use a random number generation technique (like [simplex noise](https://github.com/lmas/opensimplex) that allows you to ask for random numbers at particular seed values.\n",
    "* In the example below, the `end_frame` variable specifies how many frames the animation should last; the `fraction` variable inside the function calculates a value from 0 to 1 that indicates how much of the animation has passed given a particular `step` value. This is then used as a multiplier on the random values added to the `x` and `y` coordinates of each glyph. (Subtracting this value from 1, as I did below, essentially \"reverses\" the animation, moving the characters back into position instead of moving them progressively further away.)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 136,
   "metadata": {},
   "outputs": [],
   "source": [
    "end_frame = 60\n",
    "def render(step=0):\n",
    "    np.random.seed(12345)\n",
    "    page = document(300, 90, 'pt').addpage()\n",
    "    bg_pen = shape().nostroke().fill(rgb(255, 255, 255))\n",
    "    page.place(bg_pen.rectangle(0, 0, 300, 90))\n",
    "    pen = shape().nostroke().fill(rgb(0, 0, 0))\n",
    "    factor = 36 / f.density\n",
    "    cx = 0\n",
    "    fraction = 1 - (step / end_frame)\n",
    "    for ch in s:\n",
    "        glyph_path = (glyphcommands(f, ch)\n",
    "                      .scale(factor)\n",
    "                      .translate(cx + (np.random.normal(0, 30) * fraction),\n",
    "                                 60 + (np.random.normal(0, 30) * fraction)))\n",
    "        page.place(pen.path(glyph_path))\n",
    "        cx += advancefor(f, ch) * factor\n",
    "    return page"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Render and show at a particular frame:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 146,
   "metadata": {
    "scrolled": false
   },
   "outputs": [
    {
     "data": {
      "image/svg+xml": [
       "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" version=\"1.1\" width=\"300pt\" height=\"90pt\">\n",
       "<title>Untitled</title>\n",
       "<rect x=\"0\" y=\"0\" width=\"300\" height=\"90\" fill=\"rgb(255,255,255)\"/>\n",
       "<path d=\"M16.0094,75.8242 L12.8414,75.8242 L12.8414,67.5802 Q12.8414,66.9322,12.8774,66.0682 Q12.9134,65.2042,13.0214,64.5922 L12.8054,64.5922 Q11.9774,65.8162,10.5194,66.6802 Q9.0614,67.5442,6.7574,67.5442 Q3.2654,67.5442,1.0874,65.0422 Q-1.0906,62.5402,-1.0906,57.5722 Q-1.0906,52.6042,1.1234,50.0662 Q3.3374,47.5282,6.8654,47.5282 Q9.1334,47.5282,10.5734,48.3922 Q12.0134,49.2562,12.8774,50.5162 L13.0214,50.5162 L13.4894,47.8882 L16.0094,47.8882 L16.0094,75.8242 z M3.4454,62.9902 Q4.7054,64.9162,7.3694,64.9162 Q10.3214,64.9162,11.5814,63.2782 Q12.8414,61.6402,12.8774,58.2562 L12.8774,57.6082 Q12.8774,53.9362,11.6534,52.0462 Q10.4294,50.1562,7.2974,50.1562 Q4.7054,50.1562,3.4454,52.1902 Q2.1854,54.2242,2.1854,57.6442 Q2.1854,61.0642,3.4454,62.9902 z\" fill=\"rgb(0,0,0)\"/>\n",
       "<path d=\"M30.3684,32.368 L33.5364,32.368 L33.5364,51.664 L30.9444,51.664 L30.4764,49.108 L30.3324,49.108 Q29.3964,50.62,27.7404,51.322 Q26.0844,52.024,24.2124,52.024 Q20.7204,52.024,18.9564,50.35 Q17.1924,48.676,17.1924,45.004 L17.1924,32.368 L20.3964,32.368 L20.3964,44.788 Q20.3964,49.396,24.6804,49.396 Q27.8844,49.396,29.1264,47.596 Q30.3684,45.796,30.3684,42.412 L30.3684,32.368 z\" fill=\"rgb(0,0,0)\"/>\n",
       "<path d=\"M80.6427,61.7671 Q82.3707,61.2811,84.2427,61.2811 Q87.7707,61.2811,89.4627,62.8291 Q91.1547,64.3771,91.1547,67.7611 L91.1547,80.9011 L88.8507,80.9011 L88.2387,78.1651 L88.0947,78.1651 Q86.8347,79.7491,85.4307,80.5051 Q84.0267,81.2611,81.6147,81.2611 Q78.9867,81.2611,77.2587,79.8751 Q75.5307,78.4891,75.5307,75.5371 Q75.5307,72.6571,77.7987,71.0911 Q80.0667,69.5251,84.7827,69.3811 L88.0587,69.2731 L88.0587,68.1211 Q88.0587,65.7091,87.0147,64.7731 Q85.9707,63.8371,84.0627,63.8371 Q82.5507,63.8371,81.1827,64.2871 Q79.8147,64.7371,78.6267,65.3131 L77.6547,62.9371 Q78.9147,62.2531,80.6427,61.7671 z M88.0227,73.1971 L88.0227,71.4691 L85.1787,71.5771 Q81.5787,71.7211,80.1927,72.7291 Q78.8067,73.7371,78.8067,75.5731 Q78.8067,77.1931,79.7967,77.9491 Q80.7867,78.7051,82.3347,78.7051 Q84.7467,78.7051,86.3847,77.3551 Q88.0227,76.0051,88.0227,73.1971 z\" fill=\"rgb(0,0,0)\"/>\n",
       "<path d=\"M74.5276,45.7222 Q76.0216,44.5702,78.0376,44.5702 Q78.5776,44.5702,79.1896,44.6242 Q79.8016,44.6782,80.3056,44.7862 L79.9096,47.7022 Q79.4416,47.5942,78.8656,47.5222 Q78.2896,47.4502,77.8216,47.4502 Q76.3456,47.4502,75.0496,48.2602 Q73.7536,49.0702,72.9796,50.5282 Q72.2056,51.9862,72.2056,53.9302 L72.2056,64.2262 L69.0376,64.2262 L69.0376,44.9302 L71.6296,44.9302 L71.9896,48.4582 L72.1336,48.4582 Q73.0336,46.8742,74.5276,45.7222 z\" fill=\"rgb(0,0,0)\"/>\n",
       "<path d=\"M98.3133,75.6185 Q99.1593,76.4645,100.4913,76.4645 Q101.2113,76.4645,101.9673,76.3565 Q102.7233,76.2485,103.1913,76.0685 L103.1913,78.4805 Q102.6873,78.7325,101.7513,78.8945 Q100.8153,79.0565,99.9513,79.0565 Q98.4393,79.0565,97.1613,78.5345 Q95.8833,78.0125,95.0913,76.7165 Q94.2993,75.4205,94.2993,73.0805 L94.2993,61.8485 L91.5633,61.8485 L91.5633,60.3365 L94.3353,59.0765 L95.5953,54.9725 L97.4673,54.9725 L97.4673,59.4005 L103.0473,59.4005 L103.0473,61.8485 L97.4673,61.8485 L97.4673,73.0085 Q97.4673,74.7725,98.3133,75.6185 z\" fill=\"rgb(0,0,0)\"/>\n",
       "<path d=\"M123.0718,38.1087 L123.0718,40.5567 L108.9598,40.5567 L108.9598,38.4687 L119.3278,23.7087 L109.5718,23.7087 L109.5718,21.2607 L122.8198,21.2607 L122.8198,23.6367 L112.5958,38.1087 L123.0718,38.1087 z\" fill=\"rgb(0,0,0)\"/>\n",
       "<path d=\"M121.3049,63.5057 L113.5289,44.1377 L116.9129,44.1377 L121.0889,55.1177 Q121.6289,56.5937,122.0609,57.8897 Q122.4929,59.1857,122.7089,60.3737 L122.8529,60.3737 Q123.0689,59.4737,123.5369,58.0157 Q124.0049,56.5577,124.5089,55.0817 L128.4329,44.1377 L131.8529,44.1377 L123.5369,66.0977 Q122.4929,68.8337,120.9089,70.4537 Q119.3249,72.0737,116.5169,72.0737 Q115.6529,72.0737,115.0049,71.9837 Q114.3569,71.8937,113.8889,71.7857 L113.8889,69.2657 Q114.2849,69.3377,114.8429,69.4097 Q115.4009,69.4817,116.0129,69.4817 Q117.6689,69.4817,118.6769,68.5457 Q119.6849,67.6097,120.2969,66.0617 L121.3049,63.5057 z\" fill=\"rgb(0,0,0)\"/>\n",
       "<path d=\"\" fill=\"rgb(0,0,0)\"/>\n",
       "<path d=\"M119.0154,35.1264 L119.0154,37.5744 L114.1554,37.5744 L114.1554,54.4224 L110.9874,54.4224 L110.9874,37.5744 L107.6034,37.5744 L107.6034,36.0984 L110.9874,35.0184 L110.9874,33.9024 Q110.9874,30.1584,112.6254,28.5204 Q114.2634,26.8824,117.2514,26.8824 Q118.3674,26.8824,119.3394,27.0804 Q120.3114,27.2784,120.9954,27.5304 L120.1674,30.0144 Q119.5914,29.8344,118.8354,29.6544 Q118.0794,29.4744,117.2874,29.4744 Q115.7034,29.4744,114.9294,30.5364 Q114.1554,31.5984,114.1554,33.8664 L114.1554,35.1264 L119.0154,35.1264 z\" fill=\"rgb(0,0,0)\"/>\n",
       "<path d=\"M193.2094,38.3195 Q194.3434,40.5335,194.3434,43.7375 Q194.3434,48.5255,191.9134,51.1535 Q189.4834,53.7815,185.3434,53.7815 Q182.7874,53.7815,180.7894,52.6115 Q178.7914,51.4415,177.6394,49.1915 Q176.4874,46.9415,176.4874,43.7375 Q176.4874,38.9495,178.8814,36.3575 Q181.2754,33.7655,185.4514,33.7655 Q188.0434,33.7655,190.0594,34.9355 Q192.0754,36.1055,193.2094,38.3195 z M181.0954,38.3735 Q179.7634,40.3175,179.7634,43.7375 Q179.7634,47.1575,181.1134,49.1555 Q182.4634,51.1535,185.4154,51.1535 Q188.3314,51.1535,189.6994,49.1555 Q191.0674,47.1575,191.0674,43.7375 Q191.0674,40.3175,189.6994,38.3735 Q188.3314,36.4295,185.3794,36.4295 Q182.4274,36.4295,181.0954,38.3735 z\" fill=\"rgb(0,0,0)\"/>\n",
       "<path d=\"M163.8039,67.1548 L170.7879,57.2908 L164.1279,47.8588 L167.7279,47.8588 L172.6959,55.1308 L177.6279,47.8588 L181.1919,47.8588 L174.5319,57.2908 L181.5519,67.1548 L177.9519,67.1548 L172.6959,59.4508 L167.3679,67.1548 L163.8039,67.1548 z\" fill=\"rgb(0,0,0)\"/>\n",
       "<path d=\"M245.0602,26.2496 Q246.9862,25.0256,249.5422,25.0256 Q251.9902,25.0256,253.7902,26.1056 Q255.5902,27.1856,256.5442,29.1476 Q257.4982,31.1096,257.4982,33.7376 L257.4982,35.6456 L244.2862,35.6456 Q244.3582,38.9216,245.9602,40.6316 Q247.5622,42.3416,250.4422,42.3416 Q252.2782,42.3416,253.7002,41.9996 Q255.1222,41.6576,256.6342,41.0096 L256.6342,43.7816 Q255.1582,44.4296,253.7182,44.7356 Q252.2782,45.0416,250.2982,45.0416 Q247.5262,45.0416,245.4382,43.9256 Q243.3502,42.8096,242.1802,40.6136 Q241.0102,38.4176,241.0102,35.1776 Q241.0102,32.0096,242.0722,29.7416 Q243.1342,27.4736,245.0602,26.2496 z M253.0342,29.1476 Q251.9182,27.6176,249.5062,27.6176 Q247.2382,27.6176,245.9242,29.0756 Q244.6102,30.5336,244.3582,33.1256 L254.1862,33.1256 Q254.1502,30.6776,253.0342,29.1476 z\" fill=\"rgb(0,0,0)\"/>\n",
       "<path d=\"M216.8837,53.9598 Q217.5677,54.9858,217.5677,56.5338 Q217.5677,59.3418,215.4797,60.7818 Q213.3917,62.2218,209.8637,62.2218 Q207.8477,62.2218,206.3897,61.8978 Q204.9317,61.5738,203.8157,60.9978 L203.8157,58.1178 Q204.9677,58.6938,206.6057,59.1798 Q208.2437,59.6658,209.9357,59.6658 Q212.3477,59.6658,213.4277,58.8918 Q214.5077,58.1178,214.5077,56.8218 Q214.5077,56.1018,214.1117,55.5258 Q213.7157,54.9498,212.6897,54.3738 Q211.6637,53.7978,209.7557,53.0778 Q207.8837,52.3578,206.5517,51.6378 Q205.2197,50.9178,204.4997,49.9098 Q203.7797,48.9018,203.7797,47.3178 Q203.7797,44.8698,205.7777,43.5378 Q207.7757,42.2058,211.0157,42.2058 Q212.7797,42.2058,214.3097,42.5658 Q215.8397,42.9258,217.1717,43.5018 L216.0917,46.0218 Q214.8677,45.5178,213.5357,45.1578 Q212.2037,44.7978,210.7997,44.7978 Q208.8557,44.7978,207.8297,45.4278 Q206.8037,46.0578,206.8037,47.1378 Q206.8037,47.9658,207.2717,48.5058 Q207.7397,49.0458,208.8377,49.5858 Q209.9357,50.1258,211.7717,50.8098 Q213.6077,51.4938,214.9037,52.2138 Q216.1997,52.9338,216.8837,53.9598 z\" fill=\"rgb(0,0,0)\"/>\n",
       "</svg>"
      ],
      "text/plain": [
       "<IPython.core.display.SVG object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "show(render(30))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "And preview with `widgets.interact()`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 139,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "28800178f4144f708762420aa4f94fbc",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "interactive(children=(Play(value=0, description='step', interval=33, max=60), Output()), _dom_classes=('widget…"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/plain": [
       "<function __main__.<lambda>(step)>"
      ]
     },
     "execution_count": 139,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "widgets.interact(lambda step: show(render(step)),\n",
    "                 step=widgets.Play(min=0, max=end_frame, interval=1000/30))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "(The `lambda step: show(render(step))` syntax there is a shorthand way of writing the `show_and_render()` function from above. [More on lambda functions.](https://realpython.com/python-lambda/))\n",
    "\n",
    "Finally, the code below writes each of the frames, from zero up to `end_frame`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 140,
   "metadata": {},
   "outputs": [],
   "source": [
    "for i in range(end_frame):\n",
    "    fname = f\"render/image{i:05}.png\"\n",
    "    png_data = page2png(render(i))\n",
    "    with open(fname, \"wb\") as fh:\n",
    "        fh.write(png_data)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "And then we convert those frames to mp4:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 141,
   "metadata": {},
   "outputs": [],
   "source": [
    "!ffmpeg -loglevel warning -y \\\n",
    "    -framerate 30 -f image2 -i render/image%05d.png \\\n",
    "    -vcodec libx264 -crf 10 -pix_fmt yuv420p render/output.mp4"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Dancing asemic forms (advanced)\n",
    "\n",
    "The code below makes animated versions of the glyphs described in the [k-means glyphs](kmeans-glyphs.ipynb) notebook. First, here's a copy of the necessary function for producing a single glyph:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 147,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.cluster import MiniBatchKMeans"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 148,
   "metadata": {},
   "outputs": [],
   "source": [
    "def kmeans_glyph(n_clusters=6):\n",
    "    data = np.random.uniform(low=-0.5, high=0.5, size=(500, 2))\n",
    "    km = MiniBatchKMeans(n_clusters=n_clusters, max_iter=25)\n",
    "    km.fit(data)\n",
    "    np.random.shuffle(km.cluster_centers_)\n",
    "    return Polyline(km.cluster_centers_)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The code below draws a single glyph:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 198,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/svg+xml": [
       "<svg height=\"283.4646pt\" version=\"1.1\" width=\"283.4646pt\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">\n",
       "<title>Untitled</title>\n",
       "<polygon fill=\"rgb(40,40,40)\" points=\"214.3879 162.4012 217.1304 166.0428 218.7265 170.3672 219.4218 175.1714 219.4086 180.3952 218.8092 185.9956 217.7104 191.9145 216.1851 198.0789 214.2999 204.4065 212.1195 210.809 209.7075 217.1944 207.127 223.4687 204.4411 229.5367 201.7121 235.3027 199.0021 240.6709 196.3721 245.547 193.8803 249.8392 191.5789 253.4622 189.5008 256.349 187.595 258.4905 185.1885 260.0117 180.9309 258.2724 180.4192 255.2304 180.9213 252.0067 180.9462 252.012 182.1819 247.5701 184.1411 241.8727 186.7434 234.9744 189.902 226.9925 193.5265 218.0613 197.525 208.321 201.8045 197.9147 206.2717 186.9868 210.833 175.6824 215.3947 164.1475 219.8629 152.5281 224.1434 140.9712 228.1423 129.6243 231.7652 118.6358 234.9181 108.156 237.507 98.3376 239.4387 89.3382 240.6232 81.3245 240.9804 74.4805 240.4639 69.0197 239.1294 65.1656 237.1502 62.9199 234.2501 61.9046 234.2528 61.8793 229.9193 62.2593 224.2242 64.3439 217.4462 68.0922 209.7833 73.3513 201.3799 79.9501 192.3569 87.7184 182.8248 96.4876 172.8889 106.0911 162.652 116.3633 152.2157 127.1396 141.6805 138.2556 131.1465 149.5475 120.7133 160.8518 110.4803 172.0051 100.5467 182.8443 91.0112 193.2066 81.9724 202.9296 73.528 211.8513 65.7745 219.8107 58.806 226.6489 52.7102 232.2107 47.5553 236.3501 43.3313 238.9402 43.3205 238.9172 39.6812 239.7961 36.593 238.415 35.1655 235.3476 34.9638 231.3796 35.6165 226.4662 37.0244 220.6345 39.1346 213.9818 41.9009 206.6305 45.2786 198.7131 49.2227 190.3665 53.6882 181.7295 58.6301 172.9422 64.0035 164.145 69.7634 155.4787 75.8654 147.0845 82.2652 139.1032 88.9195 131.6763 95.7858 124.9449 102.8229 119.0512 109.9918 114.1388 117.2545 110.3545 124.5729 107.8514 131.9009 106.7903 131.902 106.8157 139.3871 107.212 147.2274 108.8773 155.3522 111.6818 163.6913 115.5035 172.1725 120.2246 180.721 125.7294 189.2603 131.903 197.7126 138.6306 205.9992 145.7977 214.0408 153.2898 221.7578 160.9924 229.0705 168.7907 235.8987 176.5704 242.1624 184.2169 247.7815 191.6162 252.7113 198.631 256.8575 205.1655 260.1241 211.1247 262.4324 216.4101 263.6982 220.9454 263.7937 224.7029 262.4249 227.607 259.4858 229.1454 259.4931 229.1829 255.3542 229.4963 249.7946 229.1121 242.893 228.0891 234.771 226.4655 225.5624 224.2766 215.406 221.5574 204.4424 218.3425 192.813 214.667 180.6596 210.5655 168.1245 206.0728 155.3498 201.2238 142.4777 196.053 129.6504 190.5952 117.0098 184.8846 104.6977 178.9555 92.8557 172.8416 81.6245 166.5761 71.1446 160.1911 61.5554 153.7171 52.9953 147.1815 45.6016 140.6067 39.5124 134.0061 34.8725 127.3784 34.8393 127.3974 31.8774 120.7907 30.7046 114.337 31.2255 108.1078 33.2742 102.1932 36.6413 96.627 41.1312 91.3883 46.5768 86.4392 52.8278 81.7454 59.7411 77.2802 67.1746 73.023 74.9863 68.9571 83.0333 65.0684 91.1717 61.3443 99.2564 57.7734 107.1413 54.3454 114.6788 51.0509 121.7191 47.8824 128.1088 44.8347 133.6884 41.9089 138.2877 39.1198 141.7188 36.5193 143.7885 34.2638 144.4887 32.6923 150.0668 33.705 148.3631 37.4815 145.3393 40.6815 141.2775 43.6647 136.2752 46.6077 130.4348 49.583 123.8764 52.6259 116.7308 55.7577 109.1353 58.994 101.2302 62.3475 93.1584 65.8294 85.0642 69.4497 77.0929 73.2175 69.3909 77.1407 62.1058 81.2257 55.3859 85.4762 49.3796 89.8919 44.234 94.4667 40.0895 99.1867 37.0691 104.0346 35.2677 109.0098 34.7663 114.1666 35.6892 119.6154 38.2318 125.4483 38.1986 125.4673 42.4373 131.6438 48.1822 137.9786 55.2923 144.4006 63.6195 150.8528 73.0154 157.2874 83.3322 163.6618 94.4236 169.9363 106.1437 176.0728 118.3478 182.0344 130.8911 187.7846 143.6295 193.2874 156.4191 198.5071 169.1159 203.4081 181.5764 207.955 193.6566 212.1123 205.2127 215.8446 216.1008 219.1167 226.1762 221.8931 235.2935 224.1386 243.3055 225.8178 250.0602 226.8959 255.3931 227.341 259.0971 227.1417 259.1043 227.1792 261.0438 226.2638 261.9729 224.3745 261.9473 221.2291 260.8344 216.9824 258.668 211.8371 255.5252 205.9471 251.4888 199.4441 246.6244 192.4638 240.9991 185.1404 234.7297 177.567 227.8976 169.8599 220.5841 162.1349 212.8708 154.5079 204.8392 147.0945 196.5713 140.0105 188.1492 133.3716 179.6557 127.2937 171.1746 121.8924 162.7908 117.2831 154.5909 113.5803 146.6643 110.8966 139.1022 109.3403 131.9953 109.0119 131.9964 109.0373 125.1315 110.0825 118.1981 112.5077 111.242 116.1895 104.3168 120.9966 97.4764 126.7915 90.7736 133.4336 84.259 140.7806 77.9821 148.6895 71.9911 157.0172 66.3338 165.6206 61.0575 174.3562 56.209 183.0807 51.8354 191.6503 47.9833 199.9205 44.6997 207.7461 42.0314 214.9794 40.0253 221.4687 38.7274 227.0523 38.1762 231.5429 38.3647 234.6834 39.0132 236.1447 39.7785 236.4282 41.8621 235.8234 41.8513 235.8004 45.4772 233.5064 50.3633 229.5165 56.3012 224.03 63.1612 217.2272 70.8325 209.2792 79.211 200.3531 88.195 190.6144 97.6841 180.2272 107.5785 169.3555 117.7787 158.1625 128.1857 146.8115 138.7007 135.4654 149.225 124.2868 159.6605 113.4381 169.9095 103.0813 179.8753 93.378 189.4625 84.4888 198.5782 76.5733 207.1347 69.7894 215.0546 64.2927 222.284 60.2387 228.8228 57.8006 234.7514 57.2638 234.7541 57.2385 239.7835 58.9736 243.2978 62.7953 245.2085 68.0337 245.8767 74.3993 245.5459 81.8239 244.3574 90.2296 242.4191 99.5086 239.8285 109.5362 236.6804 120.1785 233.0686 131.2967 229.0867 142.7494 224.8283 154.3936 220.3871 166.0853 215.8568 177.6797 211.3312 189.0316 206.9045 199.9952 202.6707 210.4242 198.7245 220.1714 195.1606 229.0883 192.0748 237.0237 189.5646 243.8203 187.731 249.3039 186.6813 253.2374 186.7062 253.2427 186.3778 255.5337 186.2277 255.4175 183.396 254.2143 183.3522 254.0812 184.6179 252.5773 186.3925 250.0269 188.4975 246.6201 190.8379 242.4891 193.3381 237.748 195.9289 232.5043 198.5434 226.8628 201.1158 220.9271 203.5811 214.8007 205.8744 208.5877 207.9312 202.3934 209.6874 196.325 211.0799 190.4936 212.0478 185.0158 212.536 180.0184 212.5039 175.6422 211.9459 172.0436 210.9266 169.3615 209.5639 167.5925\"/>\n",
       "</svg>"
      ],
      "text/plain": [
       "<IPython.core.display.SVG object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "glyph = kmeans_glyph(10).scale(100).translate(50, 50)\n",
    "glyph_poly = glyph.fancy_curve(samples_per=24,\n",
    "                               thicknesses=[2.5, 1.5, 0.5, 2],\n",
    "                               tightness=-0.5)\n",
    "page = document(100, 100, 'mm').addpage()\n",
    "brush = shape().nostroke().fill(rgb(40, 40, 40))\n",
    "page.place(brush.polygon(glyph_poly))\n",
    "show(page)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The code below generates two glyphs. Because the glyphs have the same number of points, we can smoothly interpolate between them using numpy's [`np.linspace()` function](https://docs.scipy.org/doc/numpy/reference/generated/numpy.linspace.html) on the Polyline's `.vertices` (i.e., the underlying numpy array). The resulting value is an array of `n_frames` with one entry per intermediate point on the interpolation between the two forms. This `render()` function draws a fancy curve from the `Polyline` produced at each point."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 151,
   "metadata": {},
   "outputs": [],
   "source": [
    "n_frames = 25\n",
    "glyph_a = kmeans_glyph(10).scale(100).translate(50, 50)\n",
    "glyph_b = kmeans_glyph(10).scale(100).translate(50, 50)\n",
    "interp = np.linspace(glyph_a.vertices, glyph_b.vertices, n_frames+1)\n",
    "def render(step=0):\n",
    "    this_glyph = Polyline(interp[step])\n",
    "    glyph_poly = this_glyph.augment().fancy_curve(samples_per=24,\n",
    "                               thicknesses=[2.5, 1.5, 0.5, 2],\n",
    "                               tightness=-0.5)\n",
    "    page = document(100, 100, 'mm').addpage()\n",
    "    brush = shape().nostroke().fill(rgb(40, 40, 40))\n",
    "    page.place(brush.polygon(glyph_poly))\n",
    "    return page"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Here's what it looks like:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 152,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "a97fed7b82aa4422b1a73c9a29c46f7c",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "interactive(children=(Play(value=0, description='step', interval=33, max=25), Output()), _dom_classes=('widget…"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/plain": [
       "<function __main__.<lambda>(step)>"
      ]
     },
     "execution_count": 152,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "widgets.interact(lambda step: show(render(step)),\n",
    "                 step=widgets.Play(min=0, max=n_frames, step=1, interval=1000/30))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "That's one interpolation between two forms. The code in the cell below chains together multiple interpolations like this, forming one big list of interpolated forms. It ends with the same form it began with, so the video will loop seamlessly:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 153,
   "metadata": {},
   "outputs": [],
   "source": [
    "from itertools import chain\n",
    "\n",
    "# canvas\n",
    "width = 250\n",
    "height = 250\n",
    "# number of interpolations and steps per interpolation\n",
    "n_interps = 12\n",
    "n_steps = 24\n",
    "interps = []\n",
    "# generate interpolations\n",
    "start = kmeans_glyph(10)\n",
    "current = start\n",
    "for i in range(n_interps):\n",
    "    end = kmeans_glyph(10)\n",
    "    interp = np.linspace(current.vertices, end.vertices, n_steps)\n",
    "    # make the last value of this interpolation the first of the next\n",
    "    current = end \n",
    "    interps.append(interp)\n",
    "# loop back to first\n",
    "interps.append(np.linspace(current.vertices, start.vertices, n_steps))\n",
    "# flatten list of lists\n",
    "interps = list(chain(*interps))\n",
    "\n",
    "def render(step=0):\n",
    "    this_glyph = Polyline(interps[step]).scale(width).translate(width*0.5, height*0.5)\n",
    "    glyph_poly = this_glyph.augment().fancy_curve(samples_per=24,\n",
    "                               thicknesses=[5, 3, 1, 4],\n",
    "                               tightness=-0.5)\n",
    "    page = document(width, height, 'mm').addpage()\n",
    "    background = shape().nostroke().fill(rgb(255, 255, 255))\n",
    "    brush = shape().nostroke().fill(rgb(40, 40, 40))\n",
    "    page.place(background.rectangle(0, 0, width, height))\n",
    "    page.place(brush.polygon(glyph_poly))\n",
    "    return page"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The Play widget preview:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 154,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "62194901f7764b91bdd828c3bd0d14d6",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "interactive(children=(Play(value=0, description='step', interval=33, max=311), Output()), _dom_classes=('widge…"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "text/plain": [
       "<function __main__.<lambda>(step)>"
      ]
     },
     "execution_count": 154,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "widgets.interact(lambda step: show(render(step)),\n",
    "                 step=widgets.Play(min=0, max=len(interps)-1, step=1, interval=1000/30))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now to write the frames. I've put a little ad-hoc progress counter in the cell below, since it can take a bit of time to export each frame:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 250,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "312\n",
      "0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 "
     ]
    }
   ],
   "source": [
    "print(len(interps))\n",
    "for i in range(len(interps)):\n",
    "    print(i, end=\" \")\n",
    "    fname = f\"render/image{i:05}.png\"\n",
    "    png_data = page2png(render(i))\n",
    "    with open(fname, \"wb\") as fh:\n",
    "        fh.write(png_data)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Aaaand convert to mp4:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 251,
   "metadata": {},
   "outputs": [],
   "source": [
    "!ffmpeg -loglevel warning -y \\\n",
    "    -framerate 30 -f image2 -i render/image%05d.png \\\n",
    "    -vcodec libx264 -crf 10 -pix_fmt yuv420p render/output.mp4"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Benchmarking Flat `image().png()` versus CairoSVG `svg2png`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 33,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "3.61 ms ± 77.6 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n"
     ]
    }
   ],
   "source": [
    "import random\n",
    "import cairosvg\n",
    "%timeit cairosvg.svg2png(render(random.randrange(0, 30)).svg(), dpi=72)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 34,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "131 ms ± 3.6 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)\n"
     ]
    }
   ],
   "source": [
    "%timeit render(random.randrange(0, 30)).image(ppi=72, kind='rgb').png()"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.2"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
